/**
 * 
 */
package edu.ubc.javascript;

import static edu.ubc.javascript.ASTFactory.assign;
import static edu.ubc.javascript.ASTFactory.comma;
import static edu.ubc.javascript.ASTFactory.exprStmt;
import static edu.ubc.javascript.ASTFactory.get;
import static edu.ubc.javascript.ASTFactory.name;
import static edu.ubc.javascript.ASTFactory.paren;
import static edu.ubc.javascript.ASTFactory.prop;
import static edu.ubc.javascript.ContextCheck.isVariableReferenceName;

import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import com.google.javascript.jscomp.mozilla.rhino.ast.Assignment;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.AstRoot;
import com.google.javascript.jscomp.mozilla.rhino.ast.Block;
import com.google.javascript.jscomp.mozilla.rhino.ast.ExpressionStatement;
import com.google.javascript.jscomp.mozilla.rhino.ast.ForInLoop;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionCall;
import com.google.javascript.jscomp.mozilla.rhino.ast.FunctionNode;
import com.google.javascript.jscomp.mozilla.rhino.ast.Name;
import com.google.javascript.jscomp.mozilla.rhino.ast.NodeVisitor;
import com.google.javascript.jscomp.mozilla.rhino.ast.ObjectLiteral;
import com.google.javascript.jscomp.mozilla.rhino.ast.ObjectProperty;
import com.google.javascript.jscomp.mozilla.rhino.ast.PropertyGet;
import com.google.javascript.jscomp.mozilla.rhino.ast.SwitchCase;
import com.google.javascript.jscomp.mozilla.rhino.ast.VariableDeclaration;
import com.google.javascript.jscomp.mozilla.rhino.ast.VariableInitializer;

public class OpenClosuresVisitor implements NodeVisitor {

	private Stack<String> scopeNames = new Stack<String>();
	private Stack<FunctionNode> funs = new Stack<FunctionNode>();
	private int count = 0;
	private Set<String> forcedClosed = new HashSet<String>();
	private final static String PARENT_SCOPE_NAME = Config.config().getProperty("functionClosureParentScopeName");
	private final static String SCOPE_NAME = Config.config().getProperty("functionClosurePropertyName");
	private static int fnCount = 0;
	private static final String TEMP_NAME = Config.config().getProperty("temporaryVariablePrefix");
	private static final String FUNCTION_PREFIX = Config.config().getProperty("functionRenamePrefix");
	private Stack<String> fnNames = new Stack<String>();
	public List<FunctionNode> innerFunctions = new LinkedList<FunctionNode>();
	private ReflectiveAstTransformer tx;
	private FunctionNodeClosureInfoVisitor fnciv;
	
	public OpenClosuresVisitor(FunctionNodeClosureInfoVisitor fnciv, ReflectiveAstTransformer tx) {
		this.fnciv = fnciv;
		this.tx = tx;
	}
	
	public static void reset() {
		fnCount = 0;
	}
	
	@Override
	public boolean visit(AstNode astNode) {
		if(astNode instanceof PropertyGet) {
			PropertyGet pg = (PropertyGet) astNode;
			if(pg.getRight() instanceof Name) {
				if(pg.getLeft() instanceof Name) {
					if(pg.getLeft().getString().equals("g") && (pg.getRight().getString().equals("length"))) {
						System.out.print("");
					}
				}
			}
		}
		if(astNode instanceof VariableDeclaration) {
			return handleVariableDeclaration((VariableDeclaration) astNode);
		} else if(astNode instanceof FunctionNode) {
			return handleFunctionNode((FunctionNode) astNode);
		} else if(astNode instanceof Name) {
			handleName((Name) astNode);
		}
		return true;	
	}
	
	private void handleName(Name name) {
		if(!isVariableReferenceName(name)) {
			return;
		}
		Tag tag = TagManager.getUniqueTag(name, "scope");
		if(name.getString().equals("cF")) {
			System.out.print("");
		}
		if(tag != null) {
			String scopeString = tag.getValue().toString();
			Tag t = TagManager.getUniqueTag(name, "isClosed");
			if(!scopeNames.isEmpty() && scopeString.equals("local") && ((t != null) || forcedClosed.contains(scopeNames.peek() + "." + name.getString()))) {
				System.out.print("");
			}
			if(!scopeNames.isEmpty() && fnciv.isOnClosurePath(funs.peek())) { //  )
				if(scopeString.startsWith("up") || (scopeString.equals("local") && ((t != null) || forcedClosed.contains(scopeNames.peek() + "." + name.getString()))) ) {
					//Name target = name(scopeNames.peek() + addLookup(name));
					Name target = name(addLookup(name));
					PropertyGet pg = get(target, name.getIdentifier());
					tx.replaceNode(name.getParent(), name, pg);
				}
			} 
		}
	}
	
	
	
	private boolean handleFunctionNode(FunctionNode fn) {
		AstNode parent = fn.getParent();
		recordInnerFunction(parent, fn);
		ObjectProperty parentScope = null;
		if(!this.scopeNames.isEmpty()) {
			parentScope = prop(name(PARENT_SCOPE_NAME), name(scopeNames.peek()));
		} 
		String outerScope = null;
		if(!scopeNames.isEmpty()) {
			outerScope = scopeNames.peek();
		}
		String scopeName = SCOPE_NAME + (++count);
		scopeNames.push(scopeName);
		funs.push(fn);
		declareClosureVar(fn); 
		handleFunctionParams(fn);
		String fnName = handleFunctionName(fn);
		
		if(!(fn.getParent() instanceof AstRoot)) {
			if(!(parent instanceof Block)) {
				handleFunctionExpression(parentScope, fn);
			} else {
				AstNode rvalue;
				ExpressionStatement callStatement = ASTFactory.parseStmt(ExpressionStatement.class, getScopedName() + ".clone()");
				
				if(fnciv.isOnClosurePath(fn)) {
					Assignment assign = assign(name(TEMP_NAME), callStatement.getExpression());
					ObjectLiteral ol = new ObjectLiteral();
					ObjectLiteral o2 = new ObjectLiteral();
					ObjectProperty value = ASTFactory.prop(ASTFactory.name("value"), ol);
					ObjectProperty enumerable = ASTFactory.prop(ASTFactory.name("enumerable"), ASTFactory.name("false"));
					o2.addElement(value);
					o2.addElement(enumerable);
					FunctionCall odp = ASTFactory.call("Object.defineProperty", ASTFactory.name("_tmp"), ASTFactory.string("__closure__"), o2);
					
					FunctionNode parentFn = null;
					if(funs.size() > 1) {
						parentFn = funs.get(funs.size()-2);
					}
					
					if((parentFn != null) && (fnciv.isOnClosurePath(parentFn))) {
						if(parentScope != null) {
							ol.addElement(parentScope);
						}
						rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
					} else {
						rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
					}
				} else {
					rvalue = callStatement.getExpression();
				}
				String funAccess = outerScope + '.' + fnName;
				Name tmp = name(funAccess);
				Name constructor = name(funAccess + ".prototype.constructor");
				forcedClosed.add(funAccess);
				Assignment construct = assign(constructor, tmp);
				Assignment assign = ASTFactory.assign(tmp, rvalue);
				fn.setFunctionName(name(getScopedName()));
				tx.replaceNode(fn.getParent(), fn, exprStmt(comma(assign, construct)));
				/*String funAccess = outerScope + '.' + fnName;
				Name tmp = name(funAccess);
				forcedClosed.add(funAccess);
				Name tmp2 = name(funAccess);
				Name tmpTarget = name(funAccess);
				PropertyGet pg = get(tmpTarget, SCOPE_NAME);
				ObjectLiteral ol = new ObjectLiteral();
				if(parentScope != null) {
					ol.addElement(parentScope);
				}
				InfixExpression fstComma = comma(assign(tmp, fn), comma(assign(pg, ol), tmp2));
				if(funs.size() > 1) {
					FunctionNode parentFn = funs.get(funs.size()-2);
					if(fnciv.isOnClosurePath(parentFn) && (parent instanceof Block)) {
						tx.replaceNode(parent, fn, exprStmt(fstComma));
					} 
				}*/
			}
		}
		
		fn.getBody().visit(this);
		
		scopeNames.pop();
		fnNames.pop();
		funs.pop();
		return false;
	}
	
	private boolean handleVariableDeclaration(VariableDeclaration vd) {
		if(scopeNames.isEmpty()) {
			return true;
		}
		List<AstNode> vars = new LinkedList<AstNode>();
		for(VariableInitializer vi : vd.getVariables()) {
			if(vi.getInitializer() != null) {
				Name id = (Name) vi.getTarget();
				Tag t = TagManager.getUniqueTag(id, "isClosed");
				Name name = null;
				if(t != null) {
					name = name(addLookup(id) + '.' + id.getIdentifier());
				} else {
					name = name("var " + id.getIdentifier());
				}
				Assignment assign = assign(name, vi.getInitializer());
				vars.add(assign);
			} else if(vd.getParent() instanceof ForInLoop) {
				Name id = (Name) vi.getTarget();
				Tag t = TagManager.getUniqueTag(id, "isClosed");
				Name name = null;
				if(t != null) {
					name = name(addLookup(id) + '.' + id.getIdentifier());
				} else {
					name = name("var " + id.getIdentifier());
				}
				vars.add(name);
			} else {
				Name id = (Name) vi.getTarget();
				Tag t = TagManager.getUniqueTag(id, "isClosed");
				Name name = null;
				if(t == null) {
					name = name("var " + id.getIdentifier());
					vars.add(name);
				} 
			}
		}
		AstNode commas = ASTFactory.commaList(vars);
		if((vd.getParent() instanceof Block) || (vd.getParent() instanceof SwitchCase) || (vd.getParent() instanceof AstRoot)) {
			tx.replaceNode(vd.getParent(), vd, exprStmt(commas));
		}
		else {
			tx.replaceNode(vd.getParent(), vd, commas);
		}
		return true;
	}
	
	private void handleFunctionDeclaration(String outerScope, ObjectProperty parentScope, FunctionNode fn) {
		
	}
	
	private void handleFunctionExpression(ObjectProperty parentScope, FunctionNode fn) {
		
		
		//InfixExpression fstComma = comma(assign, comma(odp, name(TEMP_NAME)));
		AstNode rvalue;
		ExpressionStatement callStatement = ASTFactory.parseStmt(ExpressionStatement.class, fn.getName() + ".clone()");
		
		if(fnciv.isOnClosurePath(fn)) {
			Assignment assign = assign(name(TEMP_NAME), callStatement.getExpression());
			ObjectLiteral ol = new ObjectLiteral();
			ObjectLiteral o2 = new ObjectLiteral();
			ObjectProperty value = ASTFactory.prop(ASTFactory.name("value"), name("undefined"));
			ObjectProperty enumerable = ASTFactory.prop(ASTFactory.name("enumerable"), ASTFactory.name("false"));
			o2.addElement(value);
			o2.addElement(enumerable);
			FunctionCall odp = ASTFactory.call("Object.defineProperty", ASTFactory.name("_tmp"), ASTFactory.string("__outer__"), o2);
			
			FunctionNode parentFn = null;
			if(funs.size() > 1) {
				parentFn = funs.get(funs.size()-2);
			}
			
			if((parentFn != null) && (fnciv.isOnClosurePath(parentFn))) {
				if(parentScope != null) {
					value.setRight(parentScope.getRight());
				}
				rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
			} else {
				rvalue = comma(assign, comma(odp, name(TEMP_NAME)));
			}
		} else {
			rvalue = fn;//callStatement.getExpression();
		}
		tx.replaceNode(fn.getParent(), fn, paren(rvalue));
	}
	
	private String handleFunctionName(FunctionNode fn) {
		String fnName = fn.getName();
		String scopedName = fnName;
		if((fnName == null) || fnName.equals("") ) {
			fnName = FUNCTION_PREFIX + (fnCount++);
			scopedName = getScopedName() + "_" + fnName;
			fn.setFunctionName(name(scopedName));
		}
		fnNames.push(fnName);
		return fnName;
	}
	
	private void handleFunctionParams(FunctionNode fn) {
		Block block = (Block) fn.getBody();
		List<AstNode> ps = fn.getParams();
		for(AstNode param : ps) {
			Name pName = (Name) param;
			Tag t = TagManager.getUniqueTag(pName, "isClosed");
			if(t != null) {
				String pString = pName.getIdentifier();
				ExpressionStatement expr = ASTFactory.parseStmt(ExpressionStatement.class, scopeNames.peek() + '.' + pString + " = " + pString);
				tx.insertBefore(block, block.getFirstChild(), expr); 
			}
		}
	}
	
	private void declareClosureVar(FunctionNode fn) {
		if(fnciv.isOnClosurePath(fn)) {
			Block block = (Block) fn.getBody();
			VariableDeclaration fst = null;
			if(fn.getParent() instanceof AstRoot) {
				fst = ASTFactory.parseStmt(VariableDeclaration.class, "var " + scopeNames.peek() + " = {};");
				tx.insertBefore(block, block.getFirstChild(), exprStmt(fst));
			} else {
				fst = ASTFactory.parseStmt(VariableDeclaration.class, "var " + scopeNames.peek() + " = {" + PARENT_SCOPE_NAME + ": arguments.callee."+ PARENT_SCOPE_NAME + "};");
				ExpressionStatement snd = exprStmt(ASTFactory.parseStmt(VariableDeclaration.class, "var " + PARENT_SCOPE_NAME + " = " + scopeNames.peek() + "." + PARENT_SCOPE_NAME));
				tx.insertBefore(block, block.getFirstChild(), exprStmt(fst));
				tx.insertBefore(block, block.getFirstChild(), exprStmt(snd));
			}
		}
	}
	
	private void recordInnerFunction(AstNode parent, FunctionNode fn) {
		if(!(parent instanceof AstRoot)) {
			innerFunctions.add(fn);
		}
	}
	
	private String getScopedName() {
		String retVal = "";
		for(String s : fnNames) {
			retVal += "_" + s;
		}
		return retVal;
	}
	
	private String addLookup(AstNode node) {
		  String retVal = "";
		  Tag tag = TagManager.getUniqueTag(node, "scope");
		  if(tag == null) {
			  throw new RuntimeException("Expected scope annotation on Node");
		  }
		  if(tag.getValue().toString().startsWith("up")) {
			  String up = tag.getValue().toString().substring(2);
			  int upCount = Integer.parseInt(up);
			  int i=0;
			  if(upCount > 0) {
				  retVal += PARENT_SCOPE_NAME;
				  i++;
			  } 
			  for(;i < upCount; i++) {
				  retVal += "." + PARENT_SCOPE_NAME;	  
			  }
		  } else {
			  retVal += scopeNames.peek();
		  }
		  return retVal;
	  }
	
}